package pers.qianyu.diytomcat.catalina;

import cn.hutool.core.util.*;
import cn.hutool.log.*;
import pers.qianyu.diytomcat.constants.*;
import pers.qianyu.diytomcat.http.*;
import pers.qianyu.diytomcat.servlets.*;
import pers.qianyu.diytomcat.util.*;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;

/**
 * @author mizzle rain
 * @date 2021-01-08 12:19
 */
public class HttpProcessor {
    public void execute(Socket socket, Request request, Response response) {
        try {
            String uri = request.getUri();
            if (StrUtil.isEmpty(uri)) {
                return;
            }

            // 准备 Session
            String id = request.getJsessionIdFromCookie();
            HttpSession session = SessionManager.getSession(id, request, response);
            request.setSession(session);

            String className = request.getContext().getServletClassName(uri);
            HttpServlet workServlet;
            if (Objects.nonNull(className)) {
                workServlet = InvokerServlet.getInstance();
            } else if (uri.endsWith(".jsp")) {
                workServlet = JspServlet.getInstance();
            } else {
                workServlet = DefaultServlet.getInstance();
            }
            List<Filter> filters = request.getContext().getMatchedFilter(uri);
            ApplicationFilterChain filterChain = new ApplicationFilterChain(filters, workServlet);
            filterChain.doFilter(request, response);

            if (request.isForward()) {
                return;
            }

            if (response.getStatus() == HttpCode.CODE_200) {
                handle200(request, response, socket);
            } else if (response.getStatus() == HttpCode.CODE_404) {
                handle404(uri, socket);
            } else if (response.getStatus() == HttpCode.CODE_302) {
                handle302(response, socket);
            }
        } catch (Exception e) {
            LogFactory.get().error(e);
            handle500(socket, e);
        } finally {
            if (!socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    LogFactory.get().error(e);
                }
            }
        }
    }

    private void handle302(Response response, Socket socket) throws IOException {
        OutputStream out = socket.getOutputStream();
        String redirectPath = response.getRedirectPath();
        String content = StrUtil.format(Headers.RESPONSE_HEADER_302, redirectPath);
        out.write(content.getBytes(StandardCharsets.UTF_8));
    }

    private void handle500(Socket socket, Exception e) {
        try {
            StackTraceElement[] stackTrace = e.getStackTrace();
            StringBuffer sb = new StringBuffer();
            sb.append(e.toString()).append("\r\n");
            for (StackTraceElement traceElement : stackTrace) {
                sb.append("\t").append(traceElement.toString()).append("\r\n");
            }
            String message = e.getMessage();
            if (Objects.nonNull(message) && message.length() > 20) {
                message = message.substring(0, 20);
            }
            String content = StrUtil.format(TextFormat.TEXT_500, message, e.toString(), sb.toString());
            OutputStream out = socket.getOutputStream();
            String body = Headers.RESPONSE_HEADER_500 + content;
            out.write(body.getBytes(StandardCharsets.UTF_8));
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }

    private void handle404(String uri, Socket socket) throws IOException {
        String content = StrUtil.format(TextFormat.TEXT_404, uri, uri);
        OutputStream out = socket.getOutputStream();
        String body = Headers.RESPONSE_HEADER_404 + content;
        out.write(body.getBytes(StandardCharsets.UTF_8));
    }

    private void handle200(Request request, Response response, Socket socket) throws IOException {
        OutputStream out = socket.getOutputStream();
        String cookiesHeader = response.getCookiesHeader();
        boolean gzip = isGzip(request, response.getBody(), response.getContentType());
        String formatter = gzip ? Headers.RESPONSE_HEADER_200_GZIP : Headers.RESPONSE_HEADER_200_WITH_COOKIE;
        String headers;
        if (!StrUtil.isEmpty(cookiesHeader)) {
            headers = StrUtil.format(formatter, response.getContentType(), cookiesHeader);
        } else {
            headers = StrUtil.format(formatter, response.getContentType());
        }
        byte[] headersBytes = headers.getBytes(StandardCharsets.UTF_8);
        byte[] contentBytes = gzip ? ZipUtil.gzip(response.getBody()) : response.getBody();
        byte[] result = new byte[headersBytes.length + contentBytes.length];
        System.arraycopy(headersBytes, 0, result, 0, headersBytes.length);
        System.arraycopy(contentBytes, 0, result, headersBytes.length, contentBytes.length);
        out.write(result);
    }

    private boolean isGzip(Request req, byte[] body, String mimeType) {
        String acceptEncoding = req.getHeader("Accept-Encoding");
        if (!StrUtil.containsAny(acceptEncoding, "gzip")) {
            return false;
        }
        Connector connector = req.getConnector();
        if (mimeType.contains(";")) {
            mimeType = StrUtil.subBefore(mimeType, ";", false);
        }
        if (!Objects.equals("on", connector.getCompression())) {
            return false;
        }
        if (body.length < connector.getCompressionMinSize()) {
            return false;
        }
        String userAgentsStr = connector.getNoCompressionUserAgents();
        String[] userAgents = userAgentsStr.split(",");
        for (String userAgent : userAgents) {
            String reqUserAgent = req.getHeader("User-Agent");
            if (StrUtil.containsAny(reqUserAgent, userAgent.trim())) {
                return false;
            }
        }
        String compressableMimeType = connector.getCompressableMimeType();
        String[] split = compressableMimeType.split(",");
        if (!ArrayUtil.contains(split, mimeType)) {
            return false;
        }
        return true;
    }
}
